package de.felixroske.jfxsupport;

import com.sun.javafx.webkit.WebConsoleListener;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Worker;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import netscape.javascript.JSObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * The Class AbstractJavaFxApplicationSupport.
 *
 * @author Felix Roske
 */
public abstract class AbstractJavaFxApplicationSupport extends Application {
    protected static Logger LOGGER = LoggerFactory.getLogger(AbstractJavaFxApplicationSupport.class);

    protected static String[] savedArgs = new String[0];

    protected static Class<? extends AbstractFxmlView> savedInitialView;

    protected static ConfigurableApplicationContext applicationContext;

    protected static SplashScreen splashScreen;

    protected static List<Image> icons = new ArrayList<>();

    protected final BooleanProperty appCtxLoaded = new SimpleBooleanProperty(false);

    public static Stage getStage() {
        return GUIState.getStage();
    }

    public static Scene getScene() {
        return GUIState.getScene();
    }

    public static HostServices getAppHostServices() {
        return GUIState.getHostServices();
    }

    public static SystemTray getSystemTray() {
        return GUIState.getSystemTray();
    }

    /**
     * @param window The FxmlView derived class that should be shown.
     * @param mode   See {@code javafx.stage.Modality}.
     */
    public static void showView(final Class<? extends AbstractFxmlView> window, final Modality mode) {
        final AbstractFxmlView view = applicationContext.getBean(window);
        Stage newStage = new Stage();

        Scene newScene;
        if (view.getView().getScene() != null) {
            // This view was already shown so
            // we have a scene for it and use this one.
            newScene = view.getView().getScene();
        } else {
            newScene = new Scene(view.getView());
        }

        newStage.setScene(newScene);
        newStage.initModality(mode);
        newStage.initOwner(getStage());
        newStage.setTitle(view.getDefaultTitle());
        newStage.initStyle(view.getDefaultStyle());

        newStage.showAndWait();
    }

    protected void loadIcons(ConfigurableApplicationContext ctx) {
        try {
            JavaFxProperties properties = ctx.getBean(JavaFxProperties.class);
            final List<String> fsImages = properties.getIcons();

            if (!fsImages.isEmpty()) {
                fsImages.forEach((s) ->
                        {
                            Image img = new Image(getClass().getResource(s).toExternalForm());
                            icons.add(img);
                        }
                );
            } else { // add factory images
                icons.add(new Image(getClass().getResource("/icons/gear_16x16.png").toExternalForm()));
                icons.add(new Image(getClass().getResource("/icons/gear_24x24.png").toExternalForm()));
                icons.add(new Image(getClass().getResource("/icons/gear_36x36.png").toExternalForm()));
                icons.add(new Image(getClass().getResource("/icons/gear_42x42.png").toExternalForm()));
                icons.add(new Image(getClass().getResource("/icons/gear_64x64.png").toExternalForm()));
            }
        } catch (Exception e) {
            LOGGER.error("Failed to load icons: ", e);
        }


    }

    /*
     * (non-Javadoc)
     *
     * @see javafx.application.Application#init()
     */
    @Override
    public void init() {
        CompletableFuture.supplyAsync(() -> SpringApplication.run(this.getClass(), savedArgs)).whenComplete((ctx, throwable) -> {
            if (throwable != null) {
                LOGGER.error("Failed to load spring application context: ", throwable);
                Platform.runLater(() -> showErrorAlert(throwable));
            } else {
                Platform.runLater(() -> {
                    loadIcons(ctx);
                    launchApplicationView(ctx);
                });
            }
        });
    }

    @Override
    public void start(final Stage stage) throws Exception {
        GUIState.setStage(stage);
        GUIState.setHostServices(this.getHostServices());
        final Stage splashStage = new Stage(StageStyle.UNDECORATED);

        if (AbstractJavaFxApplicationSupport.splashScreen.visible()) {
            final Scene splashScene = new Scene(splashScreen.getParent());
            splashStage.setScene(splashScene);
            splashStage.show();
        }

        final Runnable showMainAndCloseSplash = () -> {
            showInitialView();
            if (AbstractJavaFxApplicationSupport.splashScreen.visible()) {
                splashStage.hide();
                splashStage.setScene(null);
            }
        };

        synchronized (this) {
            if (appCtxLoaded.get()) {
                // Spring ContextLoader was faster
                Platform.runLater(showMainAndCloseSplash);
            } else {
                appCtxLoaded.addListener((ov, oVal, nVal) -> {
                    Platform.runLater(showMainAndCloseSplash);
                });
            }
        }

    }

    /**
     * Show initial view.
     */
    protected void showInitialView() {
        JavaFxProperties.StageProperties properties = applicationContext.getBean(JavaFxProperties.class).getStage();
        final String stageStyle = properties.getStyle();
        if (stageStyle != null) {
            GUIState.getStage().initStyle(StageStyle.valueOf(stageStyle.toUpperCase()));
        } else {
            GUIState.getStage().initStyle(StageStyle.DECORATED);
        }

        beforeInitialView();
        setUpcallService();
        showView(savedInitialView);
    }

    /**
     * Launch application view.
     */
    protected void launchApplicationView(final ConfigurableApplicationContext ctx) {
        AbstractJavaFxApplicationSupport.applicationContext = ctx;
        GenericApplicationContext gac = (GenericApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = gac.getDefaultListableBeanFactory();

        JavaFxProperties properties = applicationContext.getBean(JavaFxProperties.class);
        boolean useWebView = properties.isUseWebView();
        if (useWebView) {
            beanFactory.registerBeanDefinition("webView", BeanDefinitionBuilder.genericBeanDefinition(WebView.class)
                    .getRawBeanDefinition());
            WebView webView = gac.getBean(WebView.class);
            webViewLoadUrl(webView);
        }
        appCtxLoaded.set(true);
    }

    protected String getUrl() {
        JavaFxProperties properties = applicationContext.getBean(JavaFxProperties.class);
        return properties.getServerUrl();
    }

    protected void webViewLoadUrl(WebView webView) {
        webView.getEngine().load(getUrl());
    }


    public void setUpcallService() {
        JavaFxProperties properties = applicationContext.getBean(JavaFxProperties.class);
        boolean useWebView = properties.isUseWebView();
        if (useWebView) {
            WebView webView = applicationContext.getBean(WebView.class);

            UpcallService upcallService = applicationContext.getBean(UpcallService.class);
            WebEngine webEngine = webView.getEngine();

            webEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
                if (newState == Worker.State.SUCCEEDED) {
                    JSObject jsobj = (JSObject) webEngine.executeScript("window");
                    jsobj.setMember("webEngine", upcallService);
                }
            });

            WebConsoleListener.setDefaultListener((view, message, lineNumber, sourceId) -> {
                LOGGER.info("Console: " + message + " [" + sourceId + ":" + lineNumber + "] ");
            });
        }

    }

    /**
     * Show view.
     *
     * @param newView the new view
     */
    public void showView(final Class<? extends AbstractFxmlView> newView) {
        try {
            Parent parent;
            if (newView != null) {
                final AbstractFxmlView view = applicationContext.getBean(newView);
                parent = view.getView();
            } else {
                parent = applicationContext.getBean(WebView.class);
            }

            if (GUIState.getScene() == null) {
                GUIState.setScene(new Scene(parent));
            } else {
                GUIState.getScene().setRoot(parent);
            }
            GUIState.getStage().setScene(GUIState.getScene());

            applyEnvPropsToView();

            GUIState.getStage().getIcons().addAll(icons);
            GUIState.getStage().show();

        } catch (Throwable t) {
            LOGGER.error("Failed to load application: ", t);
            showErrorAlert(t);
        }
    }

    /**
     * Show error alert that close app.
     *
     * @param throwable cause of error
     */
    protected static void showErrorAlert(Throwable throwable) {
        Alert alert = new Alert(AlertType.ERROR, throwable.getMessage());
        alert.showAndWait().ifPresent(response -> Platform.exit());
    }

    /**
     * Apply env props to view.
     */
    protected static void applyEnvPropsToView() {
        JavaFxProperties.StageProperties properties = applicationContext.getBean(JavaFxProperties.class).getStage();

        String title = properties.getTitle();
        Double width = properties.getWidth();
        Double height = properties.getHeight();
        boolean resizable = properties.isResizable();

        PropertyReaderHelper.setIfPresent(title, GUIState.getStage()::setTitle);
        PropertyReaderHelper.setIfPresent(width, GUIState.getStage()::setWidth);
        PropertyReaderHelper.setIfPresent(height, GUIState.getStage()::setHeight);
        PropertyReaderHelper.setIfPresent(resizable, GUIState.getStage()::setResizable);
    }

    /*
     * (non-Javadoc)
     *
     * @see javafx.application.Application#stop()
     */
    @Override
    public void stop() throws Exception {
        super.stop();
        if (applicationContext != null) {
            applicationContext.close();
        } // else: someone did it already
    }

    /**
     * Sets the title. Allows to overwrite values applied during construction at
     * a later time.
     *
     * @param title the new title
     */
    protected static void setTitle(final String title) {
        GUIState.getStage().setTitle(title);
    }

    /**
     * Launch app.
     *
     * @param appClass the app class
     * @param view     the view
     * @param args     the args
     */
    public static void launchApp(final Class<? extends AbstractJavaFxApplicationSupport> appClass,
                                 final Class<? extends AbstractFxmlView> view, final String[] args) {

        launchApp(appClass, view, new SplashScreen(), args);
    }

    public static void launchApp(final Class<? extends AbstractJavaFxApplicationSupport> appClass, final String[] args) {

        launchApp(appClass, null, new SplashScreen(), args);
    }

    /**
     * Launch app.
     *
     * @param appClass     the app class
     * @param view         the view
     * @param splashScreen the splash screen
     * @param args         the args
     */
    public static void launchApp(final Class<? extends AbstractJavaFxApplicationSupport> appClass,
                                 final Class<? extends AbstractFxmlView> view, final SplashScreen splashScreen, final String[] args) {
        savedInitialView = view;
        savedArgs = args;

        if (splashScreen != null) {
            AbstractJavaFxApplicationSupport.splashScreen = splashScreen;
        } else {
            AbstractJavaFxApplicationSupport.splashScreen = new SplashScreen();
        }

        if (SystemTray.isSupported()) {
            GUIState.setSystemTray(SystemTray.getSystemTray());
        }

        Application.launch(appClass, args);
    }

    /**
     * Gets called after full initialization of Spring application context
     * and JavaFX platform right before the initial view is shown.
     * Override this method as a hook to add special code for your app. Especially meant to
     * add AWT code to add a system tray icon and behavior by calling
     * GUIState.getSystemTray() and modifying it accordingly.
     * <p>
     * By default noop.
     */
    public void beforeInitialView() {

    }
}
